---
name: Create your own motions
route: /custom-motions
---

import { Playground } from 'docz';
import { Toggler } from '@element-motion/dev';
import Motion, { Move } from '@element-motion/core';
import OneFullRotation from './OneFullRotation';
import SupportingMotion from './SupportingMotion';
import * as Styled from './styled';

# Create your own motions

At its core lies a powerful orchestration layer which provides a way to collect and trigger motions,
as well as providing snapshots of the DOM to the motions.
This can be helpful for example when figuring out where you'd like to move an element.

> **Tip -** This works great with [other libraries](/advanced-usage#use-with-other-libaries) just incase you're wondering.

## Lifecyle of a motion

There are three phases of a motion:

1. `beforeAnimate()` used for any initial work needed for a motion, e.g. moving the `destination` ontop of the `origin`
2. `animate()` used for the primary motion, e.g. moving from `origin` to `destination`
3. `afterAnimate()` used for any supplementary post-motion motion, e.g. fading out

<br />

Each phase method has the same type signature:

```js
(elements, onFinish, setChildProps) => JSX.Element | void;
```

> **Tip -** Notice you can return JSX elements.
> If you do it will be created for that phase - and reconciled from the result of the following phases.
> This means if you return JSX elements in `beforeAnimate()` but not in `animate()` or `afterAnimate()` it will be removed!

### What is `elements{}`

This contains snapshot data for the `origin` and `destination` elements,
use this for doing any dynamic calculation for your motion.
Remember that a motion will always have an `origin` and `destination`.

> **Tip -** Notice there is a `render` property in `origin` and `destination` -
> this is the render prop that consumers pass through which you can use to create the same element if needed!

```js
{
  origin: {
    element: HTMLElement;
    elementBoundingBox: ElementBoundingBox;
    focalTargetElement: HTMLElement | null | undefined;
    focalTargetElementBoundingBox: ElementBoundingBox | undefined;
    render: CollectorChildrenAsFunction;
  },
  destination: {
    element: HTMLElement;
    elementBoundingBox: ElementBoundingBox;
    focalTargetElement: HTMLElement | null | undefined;
    focalTargetElementBoundingBox: ElementBoundingBox | undefined;
    render: CollectorChildrenAsFunction;
  }
}
```

> **Tip -** You can use the destination `element` to set style/classNames directly for better performance -
> but remember you'll be doing it outside of the React lifecycle.

### What is `onFinish()`

Call this method when you've completed the phase.
motions will not continue to the next stage until all motions finish the current phase.

### What is `setChildProps()`

This will set the wrapped child props.

```
setChildProps({
  style?: (prevStyles: InlineStyles) => InlineStyles | undefined;
  className?: (prevClass: string | undefined) => string | undefined;
});
```

## Focal motions

The first is a [focal motion](/focal-motions).
We are going to make a custom motion that makes the target element _do one full 360deg rotation_.

```js
import React, { Component } from 'react';
import { Collector } from '@element-motion/core';

export default class OneFullRotation extends Component {
  // Before animate is used to set up the motion.
  // Think of it as prepping the environment to efficiently start the motions
  // during the animate phase.
  beforeAnimate = (elements, onFinish, setChildProps) => {
    setChildProps({
      style: prevStyle => ({
        ...prevStyle,
        opacity: 1,
        transformOrigin: 'center',
        // We set up the initial state of the motion.
        // Here we are using the FLIP technique of priming the motion
        // first and then removing it so the animates into its destination position.
        transform: 'rotate(360deg)',
      }),
      // className is also available for more advanced styles.
      // className: (prevClassName) => 'new-class-name',
      // Interally we use emotion for this for a few motions.
    });

    onFinish();
  };

  // This is the primary phase of a motion.
  // Call onFinish() when the motion has completed.
  animate = (elements, onFinish, setChildProps) => {
    setTimeout(onFinish, 400);

    setChildProps({
      style: prevStyle => ({
        ...prevStyle,
        transition: 'transform 400ms',
        transform: 'none',
      }),
    });
  };

  render() {
    const { children } = this.props;

    return (
      // The collector component passes all data up to the
      // nearest Motion parent.
      <Collector
        data={{
          // Motion is the only allowed action.
          action: 'motion',
          payload: {
            beforeAnimate: this.beforeAnimate,
            animate: this.animate,
            // afterAnimate() is also available for any cleanup or extra motions
            // post the primary motion.
          },
        }}
      >
        {children}
      </Collector>
    );
  }
}
```

<Styled.Container>
  <Toggler interval>
    {toggler => (
      <Motion triggerSelfKey={toggler.shown}>
        <OneFullRotation>
          {({ ref, ...props }) => (
            <Styled.InlineBlock
              alignSelf="flex-start"
              {...props}
              ref={ref}
              onClick={() => toggler.toggle()}
            >
              click me
            </Styled.InlineBlock>
          )}
        </OneFullRotation>
      </Motion>
    )}
  </Toggler>
</Styled.Container>

> **Tip -** motions are fired from the destination `Motion` element using the origin motions,
> this allows us to have fine control over what motion should happen for the particular destination.
> Depending where we are in our application we can have different motions occur to the same destination.

### Composing motions

Composability is a primary feature - with a small change we can compose the two motions together.
If we introduce [Move](/move) we can make the element rotate and move to the new destination.

```diff
beforeAnimate = (elements, onFinish, setChildProps) => {
  setChildProps({
    style: prevStyle => ({
      ...prevStyle,
      opacity: 1,
      transformOrigin: 'center',
-      transform: 'rotate(360deg)',
+      transform: `${prevStyle.transform} rotate(360deg)`,
    }),
  });

  onFinish();
};
```

<Styled.Container>
  <Toggler interval>
    {toggler => (
      <Motion triggerSelfKey={toggler.shown}>
        <Move>
          <OneFullRotation>
            {({ ref, ...props }) => (
              <Styled.InlineBlock
                alignSelf={toggler.shown ? 'flex-end' : 'flex-start'}
                {...props}
                ref={ref}
                onClick={() => toggler.toggle()}
              >
                click me
              </Styled.InlineBlock>
            )}
          </OneFullRotation>
        </Move>
      </Motion>
    )}
  </Toggler>
</Styled.Container>

## Supporting motions

Sometimes we want to [support the primary focal motion](/supporting-motion),
for example the [Swipe](/swipe) motion creates a new element that swipes over the screen.

This is different to a regular focal motion because it _creates_ new elements through the motion lifecycle.
We can do this by _returning elements_ from a motion phase.

For this example let's make something similar to swipe except it starts from the middle of the screen.

```js
import React from 'react';
import { Collector } from '@element-motion/core';

export default class SupportingMotion extends React.Component {
  beforeAnimate = (elements, onFinish, setChildProps) => {
    onFinish();

    // Again emphasizing the FLIP technique, we prep the "final" position
    // of this supporting element first and prep the motion, in the case
    // the scaleX(0) transform.
    return (
      <div
        aria-hidden="true"
        style={{
          position: 'fixed',
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          backgroundColor: '#468cee',
          zIndex: 9999999,
          transform: 'scaleX(0)',
        }}
      />
    );
  };

  animate = (elements, onFinish, setChildProps) => {
    setTimeout(onFinish, 400);

    // We then set the transition style and remove the transform, starting the motion.
    // We call onFinish() after 400ms.
    return (
      <div
        aria-hidden="true"
        style={{
          position: 'fixed',
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          backgroundColor: '#468cee',
          zIndex: 9999999,
          transform: 'none',
          transition: 'transform 400ms',
        }}
      />
    );
  };

  afterAnimate = (elements, onFinish, setChildProps) => {
    setTimeout(onFinish, 200);

    // After motions have finished we "fade out" the supplementary motion.
    // It will be cleaned up (removed) after this phase.
    return (
      <div
        aria-hidden="true"
        style={{
          position: 'fixed',
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          backgroundColor: '#468cee',
          zIndex: 9999999,
          transform: 'none',
          transition: 'opacity 200ms',
          opacity: 0,
        }}
      />
    );
  };

  render() {
    const { children } = this.props;

    return (
      <Collector
        data={{
          action: 'motion',
          payload: {
            beforeAnimate: this.beforeAnimate,
            animate: this.animate,
            afterAnimate: this.afterAnimate,
          },
        }}
      >
        {children}
      </Collector>
    );
  }
}
```

<Styled.Container>
  <Toggler>
    {toggler => (
      <Motion triggerSelfKey={toggler.shown}>
        <SupportingMotion>
          {({ ref, ...props }) => (
            <Styled.InlineBlock
              alignSelf="flex-start"
              {...props}
              ref={ref}
              onClick={() => toggler.toggle()}
            >
              click me
            </Styled.InlineBlock>
          )}
        </SupportingMotion>
      </Motion>
    )}
  </Toggler>
</Styled.Container>

> **Tip -** Having some trouble using?
> Check out [Troubleshooting](/troubleshooting) - there may some help for you there.
